/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package org.ikit.twitter;

import com.jamesmurty.utils.XMLBuilder;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import prefuse.Constants;
import prefuse.Display;
import prefuse.Visualization;
import prefuse.action.ActionList;
import prefuse.action.RepaintAction;
import prefuse.action.assignment.ColorAction;
import prefuse.action.assignment.DataColorAction;
import prefuse.action.layout.graph.ForceDirectedLayout;
import prefuse.activity.Activity;
import prefuse.controls.DragControl;
import prefuse.controls.PanControl;
import prefuse.controls.ZoomControl;
import prefuse.data.DataTypeException;
import prefuse.data.Graph;
import prefuse.data.Schema;
import prefuse.data.Table;
import prefuse.data.Tuple;
import prefuse.data.io.CSVTableReader;
import prefuse.data.io.DataIOException;
import prefuse.data.io.GraphMLWriter;
import prefuse.render.DefaultRendererFactory;
import prefuse.render.LabelRenderer;
import prefuse.util.ColorLib;
import prefuse.visual.VisualItem;

/**
 *
 * @author Bodong Chen <bodong.chen@gmail.com>
 */
public class TwitterPrefuseVisual extends JPanel {

    private static TwitterPrefuseVisual tpv;
    private final static String baseFileURI = "data/";

    //edmedia10 lak11 cni10s CPUO geoinst MLA09 npc2010
    private final static String csvFileName = "aera2011";

    /*
     * main execution class (for running as an applet)
     */
    public static void main(String[] args) {

        // load the data
        Table t = null;
        try {
            // data in CSV format, so use CSVTableReader
            t = new CSVTableReader().readTable(baseFileURI + csvFileName + ".csv");
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }

        tpv = new TwitterPrefuseVisual(t);
    }

    /*
     * Global variables and configuration parameters
     */

    public static final String NODEKEY = "uid";
    // Map of twitterers (might have some problem with from_user_id because of raw data)
    Map<String, TweepObject> tweeps = new TreeMap<String, TweepObject>();
    Map<Integer, String> idNameMap = new TreeMap<Integer, String>();
    Map<String, Integer> nameIdMap = new TreeMap<String, Integer>();
    // Map of tweets
    Map<String, List<TweetObject>> tweets = new TreeMap<String, List<TweetObject>>();
    Map<String, List<String>> rtMap = new TreeMap<String, List<String>>(); //<BeRTUser, List<RTUser>>
    Map<String, List<String>> msgMap = new TreeMap<String, List<String>>(); //<ToUser, List<FromUser>>

    /*
     * Constructor for the class
     * This is where all the important stuff happens
     */
    public TwitterPrefuseVisual(Table t) {
        parseRowTable(t);
        Graph g = buildPrefuseGraph();
        //renderPrefuseGraph(g);
        
        buildGraphmlDocument(g);

        // Build vizster xml using DOM
        buildVizsterXMLDocument();
    }

    /**
     * Parse raw data stored in a prefuse Table object,
     * which was read from a csv file.
     * @param t prefuse Table object
     */
    private void parseRowTable(Table t) {
        /*
         * Initialize name_id and id_name map
         */
        int userCount = 1;
        for (int i = 0; i < t.getRowCount(); i++) {
            Tuple row = t.getTuple(i);

            String fromUser = row.getString(2);

            int fromUserId = -1;
            try {
                fromUserId = row.getInt(4);
            } catch (prefuse.data.DataTypeException e) {
                fromUserId = userCount;
                //e.printStackTrace();
            }
            String userProfileURL = row.getString(7);
            if (!tweeps.containsKey(fromUser)) {
                userCount++;
                TweepObject aTweep = new TweepObject(fromUser, fromUserId, userProfileURL);
                tweeps.put(fromUser, aTweep);
                idNameMap.put(fromUserId, fromUser);
                nameIdMap.put(fromUser, fromUserId);
            }
        }

        /*
         * Read raw tweet data
         * Parse them
         * and put in maps
         */
        int toUserId = 0;
        for (int i = 0; i < t.getRowCount(); i++) {
            Tuple row = t.getTuple(i);

            // get tweet text
            String text = row.getString(0);
            //System.out.println(text);

            // get to_user_id
            if ( ! (row.get(1) == null || row.getString(1).equals("[]")
                    || row.getString(1).equals("")) ) {
                String toUser = row.getString(1);
                toUserId = Integer.parseInt(toUser.substring(1, toUser.length()-1));
                //Integer.parseInt(toUser);
                //System.out.println(toUserId);
            }
            // get from_user_name
            String fromUser = row.getString(2);
            // get from_user_id
            int fromUserId = nameIdMap.get(fromUser);
            
            // get created_at
            long timeSeconds = 0;
            try {
                timeSeconds = Long.parseLong(row.getString(12));//row.getLong(12);
            } catch (ArrayIndexOutOfBoundsException e1) {
                //System.out.println("no milliseconds info. use date string.");
                java.text.DateFormat df = new SimpleDateFormat("EEE, DD MMM yyyy HH:mm:ss Z");
                df = new SimpleDateFormat("DD MMM yyyy HH:mm:ss");
                try {
                    Date d = df.parse(row.getString(11));
                    timeSeconds = d.getTime();
                } catch (ParseException e) {
                    System.out.println("data string passing error 1.");
                    e.printStackTrace();
                }
            } catch (DataTypeException e2) {
                //System.out.println("no milliseconds info. use date string.");
                java.text.DateFormat df = new SimpleDateFormat("EEE, DD MMM yyyy HH:mm:ss Z");
                df = new SimpleDateFormat("DD MMM yyyy HH:mm:ss");
                try {
                    Date d = df.parse(row.getString(11));
                    timeSeconds = d.getTime();
                } catch (ParseException e) {
                    System.out.println("data string passing error 2.");
                    e.printStackTrace();
                }
            } catch (java.lang.NumberFormatException e3) {
                //System.out.println("no milliseconds info. use date string.");
                java.text.DateFormat df = new SimpleDateFormat("DD MMM yyyy HH:mm:ss");
                try {
                    Date d = df.parse(row.getString(11));
                    timeSeconds = d.getTime();
                } catch (ParseException e) {
                    System.out.println("data string passing error 2.");
                    e.printStackTrace();
                }
            }

            // Add this tweet to tweets map
            TweetObject aTweet = new TweetObject(fromUser, fromUserId, toUserId, text, timeSeconds);
            if (text.startsWith("@") && fromUserId != toUserId) { // && fromUserId!=toUserId
                aTweet.setType(TweetObject.TWEET_TYPE_MESSAGE);

                if (toUserId == 0) {
                    String toUser = text.substring(1,
                            text.indexOf(" ") > text.indexOf(":") ? text.indexOf(" ") : text.indexOf(":"));
                    if (nameIdMap.containsKey(toUser)) {
                        toUserId = nameIdMap.get(toUser);
                    } else {
                        userCount++;
                        TweepObject aTwitterer = new TweepObject(toUser, toUserId, "");
                        tweeps.put(toUser, aTwitterer);
                        idNameMap.put(toUserId, toUser);
                        nameIdMap.put(toUser, toUserId);
                    }
                }

                aTweet.setToUserId(toUserId);
                String toUser = idNameMap.get(toUserId);
                aTweet.setToUser(toUser);
                if (toUser != null & !fromUser.equals(toUser) && nameIdMap.containsKey(toUser)) {
                    //System.out.println(text + "/n" + fromUser + toUser);
                    if (msgMap.containsKey(toUser)) {
                        msgMap.get(toUser).add(fromUser);
                    } else {
                        List fromList = new ArrayList();
                        fromList.add(fromUser);
                        msgMap.put(toUser, fromList);
                    }
                }
            } else if (text.indexOf("@") > 0 && text.startsWith("RT")) {
                aTweet.setType(TweetObject.TWEET_TYPE_RETWEET);
                if (text.indexOf(':') > 0
                        && text.indexOf(':') > text.indexOf('@')
                        && text.indexOf(':') - text.indexOf('@') <= 16) {
                    String toUser = text.substring(text.indexOf('@') + 1, text.indexOf(':')).toLowerCase();
                    aTweet.setToUser(toUser);
                    if (idNameMap.containsKey(toUserId) && nameIdMap.containsKey(toUser)) {
                        aTweet.setToUserId(nameIdMap.get(toUser));
                    }

                    if (toUser != null && nameIdMap.containsKey(toUser)) {
                        if (rtMap.containsKey(toUser)) {
                            rtMap.get(toUser).add(fromUser);
                        } else {
                            List fromList = new ArrayList();
                            fromList.add(fromUser);
                            rtMap.put(toUser, fromList);
                        }
                    }
                } else {
                    System.out.println("Bad Retweet Format Error: " + text);
                    String tmpStr = text.substring(text.indexOf('@') + 1).trim();
                    // In case the problem is caused by missing ":"
                    if (tmpStr.indexOf(' ') <= 16 && tmpStr.indexOf(' ') > 0) {
                        String toUser = tmpStr.substring(0, tmpStr.indexOf(' ')).toLowerCase();
                        aTweet.setToUser(toUser);
                        if (idNameMap.get(toUserId) != null && nameIdMap.get(toUser) != null) {
                            aTweet.setToUserId(nameIdMap.get(toUser));
                        }

                        if (toUser != null && nameIdMap.containsKey(toUser)) {
                            if (rtMap.containsKey(toUser)) {
                                rtMap.get(toUser).add(fromUser);
                            } else {
                                List fromList = new ArrayList();
                                fromList.add(fromUser);
                                rtMap.put(toUser, fromList);
                            }
                        }
                    }
                }
            } else {
                aTweet.setType(TweetObject.TWEET_TYPE_ORDINARY);
            }
            List<TweetObject> currentList;
            if (tweets.containsKey(fromUser)) {
                currentList = tweets.get(fromUser);
            } else {
                currentList = new ArrayList<TweetObject>();
            }
            currentList.add(aTweet);
            tweets.put(fromUser, currentList);
        }

        /*
         * Set tweet/msg/rt counts for a tweep
         */
        Iterator it = tweets.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, List<TweetObject>> me = (Map.Entry) it.next();
            TweepObject author = (TweepObject) tweeps.get(me.getKey());
            author.setTweetCount(me.getValue().size());
        }

        it = rtMap.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, List<String>> me = (Map.Entry) it.next();
            TweepObject author = (TweepObject) tweeps.get(me.getKey());
            author.setRtCount(me.getValue().size());
        }

        it = msgMap.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, List<String>> me = (Map.Entry) it.next();
            TweepObject author = (TweepObject) tweeps.get(me.getKey());
            author.setMessageCount(me.getValue().size());
        }
    }

    /**
     * Build prefuse Graph object based on data parsed from raw csv file
     * @return prefuse Graph object
     */
    private Graph buildPrefuseGraph() {

        Schema nsch = new Schema();
        nsch.addColumn(NODEKEY, int.class);
        nsch.addColumn("name", String.class);
        nsch.addColumn("profileImageURL", String.class);
        nsch.addColumn("tweetCount", int.class);
        nsch.addColumn("msgCount", int.class);
        nsch.addColumn("rtCount", int.class);

        Schema esch = new Schema();
        esch.addColumn(Graph.DEFAULT_SOURCE_KEY, int.class);
        esch.addColumn(Graph.DEFAULT_TARGET_KEY, int.class);
        esch.addColumn("closeness", double.class);

        Table nodes = nsch.instantiate();
        Table edges = esch.instantiate();

        Iterator it = tweeps.entrySet().iterator();
        List<String> tweepNames = new ArrayList<String>(tweeps.keySet());

        Map<String, Integer> map = new HashMap<String, Integer>();

        while (it.hasNext()) {
            Map.Entry<String, TweepObject> me = (Map.Entry) it.next();
            TweepObject author = tweeps.get(me.getKey());

            int ar = nodes.addRow();
            nodes.setInt(ar, NODEKEY, author.getId());
            nodes.setString(ar, "name", author.getName());
            nodes.setString(ar, "profileImageURL", author.getProfileImageURL());
            nodes.setInt(ar, "tweetCount", author.getTweetCount());
            nodes.setInt(ar, "msgCount", author.getMessageCount());
            nodes.setInt(ar, "rtCount", author.getRtCount());
            map.put(author.getName(), ar);
        }

        for (int i = 0; i < tweepNames.size(); i++) {
            for (int j = i + 1; j < tweepNames.size(); j++) {
                double closeness = calculateUserCloseness(
                        tweepNames.get(i), tweepNames.get(j));
                if (closeness > 0) {
                    int er = edges.addRow();
                    edges.setInt(er, Graph.DEFAULT_SOURCE_KEY, map.get(tweepNames.get(i)));
                    edges.setInt(er, Graph.DEFAULT_TARGET_KEY, map.get(tweepNames.get(j)));
                    edges.setDouble(er, "closeness", closeness);
                }
            }
        }

        return new Graph(nodes, edges, false);
    }

    /**
     * Calculate closeness between two tweeps based on rt and msg between them.
     * The current algorithm is simply and undirected.
     * There are some other possibilities to explore.
     * @param author Name of a tweep
     * @param anotherAuthor Name of another tweep
     * @return closeness between those two guys based on rt and msg between them
     */
    private double calculateUserCloseness(String author, String anotherAuthor) {
        if (author.equals(anotherAuthor)) {
            return 0;
        }
        List<String> rtList1 = rtMap.get(author);
        List<String> rtList2 = rtMap.get(anotherAuthor);
        List<String> msgList1 = msgMap.get(author);
        List<String> msgList2 = msgMap.get(anotherAuthor);

        int totalRTCount = 0, totalMsgCount = 0;

        if (rtList1 != null) {
            for (String str : rtList1) {
                if (str.equals(anotherAuthor)) {
                    totalRTCount++;
                }
            }
        }

        if (rtList2 != null) {
            for (String str : rtList2) {
                if (str.equals(author)) {
                    totalRTCount++;
                }
            }
        }

        if (msgList1 != null) {
            for (String str : msgList1) {
                if (str.equals(anotherAuthor)) {
                    totalMsgCount++;
                }
            }
        }
        if (msgList2 != null) {
            for (String str : msgList2) {
                if (str.equals(author)) {
                    totalMsgCount++;
                }
            }
        }

        return totalMsgCount + 0.5 * totalRTCount;
        //return 1/(1+ totalMsgCount + 0.25*totalRTCount);
    }

    /**
     * Render the created graph
     * Adapted from the official demo of prefuse
     * @param g
     */
    private void renderPrefuseGraph(Graph g) {
        // -- 2. the visualization --------------------------------------------

        // add the graph to the visualization as the data group "graph"
        // nodes and edges are accessible as "graph.nodes" and "graph.edges"
        Visualization vis = new Visualization();
        vis.add("graph", g);
        vis.setInteractive("graph.edges", null, false);

        // -- 3. the renderers and renderer factory ---------------------------

        // draw the "name" label for NodeItems
        LabelRenderer r = new LabelRenderer("name");
        r.setRoundedCorner(8, 8); // round the corners

        // create a new default renderer factory
        // return our name label renderer as the default for all non-EdgeItems
        // includes straight line edges for EdgeItems by default
        vis.setRendererFactory(new DefaultRendererFactory(r));


        // -- 4. the processing actions ---------------------------------------

        // create our nominal color palette
        // pink for females, baby blue for males
        int[] palette = new int[] {
            ColorLib.rgb(255,180,180), ColorLib.rgb(190,190,255)
        };
        // map nominal data values to colors using our provided palette
        DataColorAction fill = new DataColorAction("graph.nodes", "gender",
                Constants.NOMINAL, VisualItem.FILLCOLOR, palette);
        // use black for node text
        ColorAction text = new ColorAction("graph.nodes",
                VisualItem.TEXTCOLOR, ColorLib.gray(0));
        // use light grey for edges
        ColorAction edges = new ColorAction("graph.edges",
                VisualItem.STROKECOLOR, ColorLib.gray(200));

        // create an action list containing all color assignments
        ActionList color = new ActionList();
        color.add(fill);
        color.add(text);
        color.add(edges);

        // create an action list with an animated layout
        ActionList layout = new ActionList(Activity.INFINITY);
        layout.add(new ForceDirectedLayout("graph"));
        layout.add(new RepaintAction());

        // add the actions to the visualization
        vis.putAction("color", color);
        vis.putAction("layout", layout);


        // -- 5. the display and interactive controls -------------------------

        Display d = new Display(vis);
        d.setSize(720, 500); // set display size
        // drag individual items around
        d.addControlListener(new DragControl());
        // pan with left-click drag on background
        d.addControlListener(new PanControl());
        // zoom with right-click drag
        d.addControlListener(new ZoomControl());

        // -- 6. launch the visualization -------------------------------------

        // create a new window to hold the visualization
        JFrame frame = new JFrame("prefuse example");
        // ensure application exits when window is closed
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(d);
        frame.pack();           // layout components in window
        frame.setVisible(true); // show the window

        // assign the colors
        vis.run("color");
        // start up the animated layout
        vis.run("layout");
    }

    /**
     * Save the graph into a graphml document (used by Gephi for example)
     * @param g prefuse Graph object
     */
    private void buildGraphmlDocument(Graph g) {
        GraphMLWriter w = new GraphMLWriter();
        try {
            w.writeGraph(g, baseFileURI + csvFileName +".graphml");
        } catch (DataIOException e) {
            e.printStackTrace();
            System.out.println("Could not write graph.  Continuing...");
        }
    }

    /**
     * Build vizster xml using DOM
     * The graphml file vizster uses is based on an old version graphml definition
     * So the automatically saved graphml from prefuse will not work
     * and I need to create this xml file by myself
     */
    private void buildVizsterXMLDocument() {
        PrintWriter writer = null;
        try {
            XMLBuilder builder = XMLBuilder.create("graph").a("directed", "0");
            Iterator it = tweeps.entrySet().iterator();
            List<String> tweepNames = new ArrayList<String>(tweeps.keySet());
            Map<String, Integer> map = new HashMap<String, Integer>();
            for (int i = 1; it.hasNext(); i++) {
                Map.Entry<String, TweepObject> me = (Map.Entry) it.next();
                TweepObject author = tweeps.get(me.getKey());
                builder = builder.e("node").a("id", Integer.toString(i)).e("att").a("name", NODEKEY).a("value", Integer.toString(author.getId())).up().e("att").a("name", "name").a("value", author.getName()).up().e("att").a("name", "profileImageURL").a("value", author.getProfileImageURL()).up().e("att").a("name", "tweetCount").a("value", Integer.toString(author.getTweetCount())).up().e("att").a("name", "msgCount").a("value", Integer.toString(author.getMessageCount())).up().e("att").a("name", "rtCount").a("value", Integer.toString(author.getRtCount())).up().up();
                map.put(author.getName(), i);
            }
            builder = builder.up();
            for (int i = 0; i < tweepNames.size(); i++) {
                for (int j = i + 1; j < tweepNames.size(); j++) {
                    double closeness = calculateUserCloseness(tweepNames.get(i), tweepNames.get(j));
                    if (closeness > 0) {
                        builder = builder.e("edge").a("source", Integer.toString(map.get(tweepNames.get(i)))).a("target", Integer.toString(map.get(tweepNames.get(j)))).t(Double.toString(closeness)).up();
                    }
                }
            }
            Properties outputProperties = new Properties();
            outputProperties.put(javax.xml.transform.OutputKeys.METHOD, "xml");
            outputProperties.put(javax.xml.transform.OutputKeys.INDENT, "yes");
            outputProperties.put("{http://xml.apache.org/xslt}indent-amount", "2");

            //System.out.println(builder.asString(outputProperties));

            writer = new PrintWriter(new FileOutputStream( baseFileURI + csvFileName + "_vizster.xml"));
            builder.toWriter(writer, outputProperties);
        } catch (TransformerException ex) {
            Logger.getLogger(TwitterPrefuseVisual.class.getName()).log(Level.SEVERE, null, ex);
        } catch (FileNotFoundException ex) {
            Logger.getLogger(TwitterPrefuseVisual.class.getName()).log(Level.SEVERE, null, ex);
        } catch (ParserConfigurationException ex) {
            Logger.getLogger(TwitterPrefuseVisual.class.getName()).log(Level.SEVERE, null, ex);
        } catch (FactoryConfigurationError ex) {
            Logger.getLogger(TwitterPrefuseVisual.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
            writer.close();
        }
    }
}
